#
# This file is a part of the open-eBackup project.
# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
# If a copy of the MPL was not distributed with this file, You can obtain one at
# http://mozilla.org/MPL/2.0/.
#
# Copyright (c) [2024] Huawei Technologies Co.,Ltd.
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#

import os
import re
import time

from common import common
from common.common import clean_dir, execute_cmd_oversize_return_value
from common.const import CMDResult
from common.logger import Logger
from db2.comm.const import Db2Const, Db2JsonConstant
from db2.comm.db2_exception import ErrCodeException
from db2.comm.error_code import Db2ErrCode
from db2.comm.util.comm_decorators import handle_restore_error
from db2.comm.util.common_util import Db2CommonUtil

LOGGER = Logger().get_logger(filename="db2.log")


class DpfUtil:
    @staticmethod
    def is_dpf_cluster_when_restore(param_dict):
        """恢复任务判断是否DPF集群"""
        cluster_type = param_dict.get("job", {}).get("targetEnv", {}).get("extendInfo", {}).get("clusterType", "")
        return cluster_type == Db2Const.DPF_CLUSTER_TYPE

    @staticmethod
    def is_catalog_node(catalog_ip):
        local_ips = Db2CommonUtil.get_local_ips()
        return catalog_ip in local_ips

    @staticmethod
    @handle_restore_error
    def execute_restore_db(restore_db_cmd):
        """执行恢复数据库命令"""
        LOGGER.info(f"Starting restore database, cmd: {restore_db_cmd}.")
        return_code, out, err = common.execute_cmd(restore_db_cmd)
        if return_code != CMDResult.SUCCESS.value:
            LOGGER.error(f"Execute restore database command failed. return code: {return_code}, "
                         f"out: {out}, err: {err}.")
            raise ErrCodeException(Db2ErrCode.FAILED_EXECUTE_COMMAND, *[restore_db_cmd, out])
        LOGGER.info(f"Execute restore database command success, output: {out}.")

    @staticmethod
    def check_rollforward_pending_after_exec_restore_db(os_user, db_name):
        """执行恢复命令后检查Rollforward pending值"""
        # 存在执行restore命令成功，但执行前滚时报”Database "xxx" is not in rollforward pending state on node“的情况
        rollforward_pending_val = Db2CommonUtil.get_rollforward_pending_val_of_db(os_user, db_name)
        if not rollforward_pending_val or rollforward_pending_val.upper() != "DATABASE":
            LOGGER.error(f"The rollforward pending value does not match after executing database restore command, "
                         f"database: {db_name}, rollforward pending: {rollforward_pending_val}.")
            raise Exception("The rollforward pending value does not match after executing database restore command")
        LOGGER.info(f"The rollforward pending value is DATABASE after restoring database.")

    @staticmethod
    def execute_rollforward_db(rollforward_db_cmd):
        """执行前滚数据库命令"""
        LOGGER.info(f"Starting rollforward database, cmd: {rollforward_db_cmd}.")
        return_code, out, err = common.execute_cmd(rollforward_db_cmd)
        if return_code != CMDResult.SUCCESS.value:
            LOGGER.error(f"Execute rollforward database command failed. return code: {return_code}, "
                         f"out: {out}, err: {err}.")
            raise ErrCodeException(Db2ErrCode.FAILED_EXECUTE_COMMAND, *[rollforward_db_cmd, out])
        if "The ROLLFORWARD command completed successfully" not in out:
            LOGGER.error("The rollforward database result information is incorrect.")
            raise ErrCodeException(Db2ErrCode.FAILED_EXECUTE_COMMAND, *[rollforward_db_cmd, out])
        LOGGER.info(f"Execute rollforward database command success.")

    @staticmethod
    @handle_restore_error
    def execute_restore_ts(restore_ts_cmd):
        """执行恢复表空间集命令"""
        LOGGER.info(f"Starting restore tablespaces, cmd: {restore_ts_cmd}.")
        return_code, out, err = common.execute_cmd(restore_ts_cmd)
        if return_code != CMDResult.SUCCESS.value:
            LOGGER.error(f"Execute restore tablespaces command failed. return code: {return_code}, "
                         f"out: {out}, err: {err}.")
            raise ErrCodeException(Db2ErrCode.FAILED_EXECUTE_COMMAND, *[restore_ts_cmd, out])
        LOGGER.info(f"Execute restore tablespaces command success, output: {out}.")

    @staticmethod
    def check_rollforward_pending_after_exec_restore_ts(os_user, db_name):
        rollforward_pending_val = Db2CommonUtil.get_rollforward_pending_val_of_db(os_user, db_name)
        if not rollforward_pending_val or rollforward_pending_val.upper() != "TABLESPACE":
            LOGGER.error(f"The rollforward pending value does not match after executing tablesapces restore command, "
                         f"database: {db_name}, rollforward pending: {rollforward_pending_val}.")
            raise Exception("The rollforward pending value does not match after executing tablesapces restore command")
        LOGGER.info(f"The rollforward pending value is TABLESPACE after restoring tablespaces.")

    @staticmethod
    def execute_rollforward_ts(rollforward_db_cmd):
        """执行前滚表空间集命令"""
        LOGGER.info(f"Starting rollforward tablespaces, cmd: {rollforward_db_cmd}.")
        return_code, out, err = common.execute_cmd(rollforward_db_cmd)
        if return_code != CMDResult.SUCCESS.value:
            LOGGER.error(f"Execute rollforward tablespaces command failed. return code: {return_code}, "
                         f"out: {out}, err: {err}.")
            raise Exception("Execute rollforward tablespaces command failed")
        if "The ROLLFORWARD command completed successfully" not in out:
            LOGGER.error("The rollforward tablespaces result information is incorrect.")
            raise Exception("The rollforward tablespaces result information is incorrect")
        LOGGER.info(f"Execute rollforward tablespaces command success.")

    @staticmethod
    def execute_offline_backup_ts(offline_backup_ts_cmd):
        """执行离线备份表空间集命令"""
        LOGGER.info(f"Starting executing an offline backup of the tablespaces, command: {offline_backup_ts_cmd}.")
        return_code, out, err = common.execute_cmd(offline_backup_ts_cmd)
        if return_code != CMDResult.SUCCESS.value:
            LOGGER.error(f"Execute the offline backup tablespaces command failed. return code: {return_code}, "
                         f"out: {out}, err: {err}.")
            raise Exception("Execute the offline backup tablespaces command failed")
        LOGGER.info(f"Execute the offline backup tablespaces command success.")

    @staticmethod
    def build_table_space_abnormal_message(all_ts_dict, bak_ts_names):
        state = "state"
        return ";".join((f"{k}:{v.get(state)}" for k, v in all_ts_dict.items()
                         if k in bak_ts_names and v.get("state") != Db2Const.NORMAL_STATE))

    @staticmethod
    def build_table_space_not_exists_message(all_ts, bak_ts_names):
        return ";".join(set(bak_ts_names).difference(set(all_ts)))

    @staticmethod
    def get_copy_cur_log_info(os_user, db_name, bak_start_time, ptn_nums: list, deploy_type=None):
        """获取备份的日志扩展信息"""
        log_chain = Db2CommonUtil.get_current_log_chain(os_user, db_name)
        copy_log_ext_info = {
            Db2JsonConstant.COPY_LOG_EXT_KEY: {
                Db2JsonConstant.LOG_CHAIN_KEY: log_chain,
                Db2JsonConstant.PTN_NUM_LOG_MAP_KEY: {}
            }
        }
        for tmp_ptn_num in ptn_nums:
            _, _, tmp_cur_log = Db2CommonUtil.get_db_data_backup_end_time_and_cur_log(
                os_user, db_name, bak_start_time, ptn_num=tmp_ptn_num, deploy_type=deploy_type)
            copy_log_ext_info[Db2JsonConstant.COPY_LOG_EXT_KEY][Db2JsonConstant.PTN_NUM_LOG_MAP_KEY][tmp_ptn_num] = \
                tmp_cur_log
        return copy_log_ext_info

    @staticmethod
    def create_empty_dir(os_user, dir_path):
        if not os.path.isdir(dir_path):
            LOGGER.info(f"The directory: {dir_path} does not exists, need create it ...")
            os.mkdir(dir_path)
            os.chown(dir_path,
                     Db2CommonUtil.get_os_user_id_by_os_user(os_user),
                     Db2CommonUtil.get_group_id_by_os_user(os_user))
        else:
            LOGGER.info(f"The directory: {dir_path} exists, need clear it.")
            clean_dir(dir_path)

    @staticmethod
    def create_new_log_path_for_restore_db(os_user, db_name, param_dict):
        job_id = param_dict.get(Db2JsonConstant.JOB, {}).get(Db2JsonConstant.JOB_ID)
        LOGGER.info(f"Creating new log path of database: {db_name}, job id: {job_id} ...")
        new_log_dir_name = f'{db_name}_{job_id}'
        ori_log_path = Db2CommonUtil.get_log_path_val_of_db(os_user, db_name)
        ori_log_path = ori_log_path.rstrip("/") if ori_log_path.endswith("/") else ori_log_path
        match_ret = re.match(r"(.+)/NODE\d{4}/", ori_log_path)
        if match_ret:
            ori_log_path_before_node = match_ret.groups()[0]
        else:
            ori_log_path_before_node = ori_log_path
        log_path_splits = ori_log_path_before_node.split("/")
        node_parent_dir_name = log_path_splits[-1]
        new_log_dir_end_reg = r"[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}"
        new_log_dir_reg = f"{db_name}_{new_log_dir_end_reg}"
        if re.match(new_log_dir_reg, node_parent_dir_name, flags=re.I):
            new_log_path = os.path.realpath(os.path.sep.join(log_path_splits[:-1] + [new_log_dir_name]))
        else:
            new_log_path = os.path.realpath(os.path.join(ori_log_path_before_node, new_log_dir_name))
        DpfUtil.create_empty_dir(os_user, new_log_path)
        LOGGER.info(f"Create new log path of database: {db_name} success.")
        return new_log_path

    @staticmethod
    def create_tmp_log_dir(cache_path, os_user):
        """创建恢复时使用的日志临时目录"""
        LOGGER.info(f"Creating restore_log directory in cache path ...")
        tmp_log_dir = os.path.realpath(os.path.join(cache_path, f"restore_log"))
        DpfUtil.create_empty_dir(os_user, tmp_log_dir)
        LOGGER.info(f"Create restore_log directory in cache path success.")
        return tmp_log_dir

    @staticmethod
    def create_tmp_primary_log_dir(cache_path, os_user):
        """创建HADR主节点前滚后非活动日志存放的临时目录"""
        LOGGER.info(f"Creating temp primary log directory in cache path ...")
        tmp_log_dir = os.path.realpath(os.path.join(cache_path, Db2Const.TMP_PRI_LOG_DIR))
        DpfUtil.create_empty_dir(os_user, tmp_log_dir)
        LOGGER.info(f"Create temp primary log directory in cache path success.")
        return tmp_log_dir

    @staticmethod
    def check_database_pending_status_for_restore(os_user, db_name):
        """
        恢复前检查数据库pending状态
        :returns: True：存在不满足的pending状态；False：不存在
        """
        pending_infos = list()
        db_cfg_out = Db2CommonUtil.get_db_cfg_for_db(os_user, db_name)
        for tmp_line in db_cfg_out.split("\n"):
            match_bak_pending_ret = re.match(r"\s*Backup pending\s*=\s*(\S+)\s*", tmp_line)
            if match_bak_pending_ret and match_bak_pending_ret.groups()[0] not in ("NO",):
                pending_infos.append(f"Backup pending={match_bak_pending_ret.groups()[0]}")
                continue
            match_roll_pending_ret = re.match(r"\s*Rollforward pending\s*=\s*(\S+)\s*", tmp_line)
            if match_roll_pending_ret and match_roll_pending_ret.groups()[0] not in ("NO",):
                pending_infos.append(f"Rollforward pending={match_roll_pending_ret.groups()[0]}")
                continue
            match_res_pending_ret = re.match(r"\s*Restore pending\s*=\s*(\S+)\s*", tmp_line)
            if match_res_pending_ret and match_res_pending_ret.groups()[0] not in ("NO",):
                pending_infos.append(f"Restore pending={match_res_pending_ret.groups()[0]}")
        if not pending_infos:
            return False, f"{db_name}"
        LOGGER.info(f"There are pending status in database: {db_name} when restore, pending infos: {pending_infos}.")
        return True, f"{db_name} {','.join(pending_infos)}"

    @staticmethod
    def get_last_commit_transaction_time(os_user, db_name):
        """获取最后的已提交事务时间"""
        Db2CommonUtil.check_os_user_with_ex(os_user)
        Db2CommonUtil.check_injection(db_name)
        query_cmd = f'su - {os_user} -c "db2 rollforward db {db_name} query status using local time"'
        return_code, out, err = execute_cmd_oversize_return_value(query_cmd)
        if return_code != CMDResult.SUCCESS.value:
            LOGGER.error(f"Execute query rollforward status command: {query_cmd} failed. return code: {return_code}, "
                         f"out: {out}, err: {err}.")
            raise ErrCodeException(Db2ErrCode.FAILED_EXECUTE_COMMAND, *[query_cmd, out])
        last_ts_times = list()
        for tmp_line in out.split("\n"):
            mat_ret = re.search(r"(\d{4}-\d{2}-\d{2}-\d{2}\.\d{2}\.\d{2}\.\d{6})\s*Local", tmp_line, flags=re.I)
            if mat_ret:
                last_ts_times.append(mat_ret.groups()[0])
        if not last_ts_times:
            err_msg = "Query rollforward status result is incorrect"
            raise ErrCodeException(Db2ErrCode.FAILED_EXECUTE_COMMAND, *[query_cmd, out], message=err_msg)
        LOGGER.info(f"Query rollforward status success, last transactions: {last_ts_times}.")
        return min(last_ts_times)

    @staticmethod
    def exec_cancel_rollforward_db(os_user, db_name):
        """执行取消前滚数据库命令"""
        Db2CommonUtil.check_os_user_with_ex(os_user)
        Db2CommonUtil.check_injection(db_name)
        query_cmd = f'su - {os_user} -c "db2 rollforward db {db_name} cancel"'
        return_code, out, err = execute_cmd_oversize_return_value(query_cmd)
        if return_code != CMDResult.SUCCESS.value:
            LOGGER.error(f"Execute cancel rollforward database command: {query_cmd} failed, return code: "
                         f"{return_code}, out: {out}, err: {err}.")
            raise ErrCodeException(Db2ErrCode.FAILED_EXECUTE_COMMAND, *[query_cmd, out])
        LOGGER.info(f"Execute cancel rollforward database: {db_name} command success.")
